/*
 *    Copyright (C) 2007-2009 Aike J Sommer
 *
 *
 *    This program is free software: you can
 *    redistribute it and/or modify it under the terms of the GNU
 *    General Public License as published by the Free Software
 *    Foundation, either version 3 of the License, or (at your
 *    option) any later version.
 *
 *    This program is distributed in the hope
 *    that it will be useful, but WITHOUT ANY WARRANTY; without even
 *    the implied warranty of MERCHANTABILITY or FITNESS FOR A
 *    PARTICULAR PURPOSE.  See the GNU General Public License for
 *    more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program.  If not, see
 *    <http://www.gnu.org/licenses/>.
 *
 *
 *    You can reach the author and get more information about this
 *    project at: http://aikesommer.name/ or
 *    http://code.google.com/p/viewncontrol
 */

package name.aikesommer.viewncontrol.web.control;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Date;
import java.util.NoSuchElementException;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpSession;
import name.aikesommer.viewncontrol.protocol.SliceStore;
import name.aikesommer.viewncontrol.protocol.VNCException;
import name.aikesommer.viewncontrol.protocol.VNCSession;
import name.aikesommer.viewncontrol.web.Pair;

/**
 *
 * @author Aike J Sommer
 */
public class ScreenViewController {

    private static final String SELF_NOTE = ScreenViewController.class.getName() + ".SELF";

    public static ScreenViewController self(HttpSession session) {
        return (ScreenViewController) session.getAttribute(SELF_NOTE);
    }

    private ScreenController screen;
    private VNCSession vnc;
    private SortedMap<Long, SliceStore.Slice> slices = Collections.
            synchronizedSortedMap(new TreeMap());
    private Pair<Long, SliceStore.Slice> cursorUpdate = null;
    private Pair<Long, SliceStore.Slice> cursorDataUpdate = null;
    private long sliceId = 0;
    private boolean checkingForMessages = false;

    public ScreenViewController() {
        FacesContext.getCurrentInstance().getExternalContext().getSessionMap().put(SELF_NOTE, this);
    }

    public ScreenController getScreen() {
        return screen;
    }

    public void setScreen(ScreenController screen) {
        this.screen = screen;
    }

    public String connect() {
        if (isConnected()) {
            throw new IllegalStateException();
        }

        if (vnc != null) {
            vnc.close();
        }
        try {
            SliceStore store = new SliceStore() {

                @Override
                public void store(SliceType type, Slice slice) {
                    switch (type) {
                        case Regular:
                            slices.put(sliceId++, slice);
                            break;
                        case Cursor:
                            cursorUpdate = new Pair(sliceId++, slice);
                            cursorUpdate.getSecond().info.put("slice-id",
                                    cursorUpdate.getFirst());
                            if (slice.hasData()) {
                                cursorDataUpdate = cursorUpdate;
                            }
                            break;
                        default:
                            throw new IllegalArgumentException("slice type: " +
                                    type.name());
                    }
                }

                @Override
                public void clear() {
                    slices.clear();
                    cursorUpdate = null;
                    cursorDataUpdate = null;
                }
            };
            vnc = new VNCSession(InetAddress.getByName(screen.getHost()), screen.getPort(),
                    screen.getPassword(), store);
        } catch (UnknownHostException ex) {
            throw new RuntimeException(ex);
        }
        try {
            vnc.connect();
        } catch (VNCException.AuthenticationFailed ex) {
            vnc = null;
            return "failed";
        } catch (IOException ex) {
            vnc = null;
            return "failed";
        }

        return "success";
    }

    public String disconnect() {
        if (vnc != null) {
            vnc.close();
            vnc = null;
        }
        return "success";
    }

    public boolean isConnected() {
        return vnc != null && vnc.isConnected();
    }

    public String getConnectedLocal() {
        return isConnected() ? "Yes" : "No";
    }

    public String getName() {
        return vnc.getName();
    }

    public int getWidth() {
        return vnc.getFramebufferWidth();
    }

    public int getHeight() {
        return vnc.getFramebufferHeight();
    }

    public String getKey() {
        return vnc.getSessionKey();
    }

    private void checkForMessage(int timeout) throws VNCException.ConnectionLost {
        synchronized (this) {
            if (checkingForMessages) {
                try {
                    wait();
                } catch (InterruptedException ex) {
                }
                return;
            }
            checkingForMessages = true;
        }

        try {
            if (timeout > 0) {
                vnc.checkForMessage(true, timeout);
            }
        } catch (NullPointerException ex) {
            throw new VNCException.ConnectionLost();
        } catch (VNCException.ConnectionLost ex) {
            vnc = null;
            throw ex;
        } finally {
            synchronized (this) {
                checkingForMessages = false;
                notifyAll();
            }
        }
    }

    public SortedMap<Long, SliceStore.Slice> getSlices(long since, int timeout) throws VNCException.ConnectionLost {
        long end = new Date().getTime() + timeout;
        int left = (int) (end - new Date().getTime());
        while (left > 0 && (slices.isEmpty() || slices.lastKey() <= since)) {
            checkForMessage(left);
            left = (int) (end - new Date().getTime());
        }
        return slices.tailMap(since + 1);
    }

    public SliceStore.Slice getCursorUpdate(long since, int timeout) throws VNCException.ConnectionLost {
        long end = new Date().getTime() + timeout;
        boolean available;
        int left;
        do {
            available =
                    (cursorUpdate != null && cursorUpdate.getFirst() > since);
            left = (int) (end - new Date().getTime());
            if (!available && left > 0) {
                checkForMessage(left);
            }
        } while (!available && left > 0);

        if (cursorDataUpdate != null && cursorDataUpdate.getFirst() > since) {
            return cursorDataUpdate.getSecond();
        } else if (cursorUpdate != null && cursorUpdate.getFirst() > since) {
            return cursorUpdate.getSecond();
        }
        return null;
    }

    public SliceStore.Slice fetchSlice(long id) {
        SliceStore.Slice result = slices.get(id);
        if (result == null) {
            if (cursorUpdate.getFirst() == id) {
                return cursorUpdate.getSecond();
            }
            if (cursorDataUpdate.getFirst() == id) {
                return cursorDataUpdate.getSecond();
            }
            throw new NoSuchElementException();
        }
        return result;
    }

    public void purgeSlice(long id) {
        slices.remove(id);
    }

    public void purgeAll() throws VNCException.ConnectionLost {
        slices.clear();
        try {
            vnc.requestFullUpdate();
        } catch (IOException ex) {
            vnc = null;
            throw new VNCException.ConnectionLost(ex);
        }
    }
}
